/**
 * Parallel Coordinates
 * <https://en.wikipedia.org/wiki/Parallel_coordinates>
 */
define(function(require) {

    var layoutUtil = require('../../util/layout');
    var axisHelper = require('../../coord/axisHelper');
    var zrUtil = require('zrender/core/util');
    var ParallelAxis = require('./ParallelAxis');
    var graphic = require('../../util/graphic');
    var matrix = require('zrender/core/matrix');
    var numberUtil = require('../../util/number');
    var sliderMove = require('../../component/helper/sliderMove');

    var each = zrUtil.each;
    var mathMin = Math.min;
    var mathMax = Math.max;
    var mathFloor = Math.floor;
    var mathCeil = Math.ceil;
    var round = numberUtil.round;

    var PI = Math.PI;

    function Parallel(parallelModel, ecModel, api) {

        /**
         * key: dimension
         * @type {Object.<string, module:echarts/coord/parallel/Axis>}
         * @private
         */
        this._axesMap = zrUtil.createHashMap();

        /**
         * key: dimension
         * value: {position: [], rotation, }
         * @type {Object.<string, Object>}
         * @private
         */
        this._axesLayout = {};

        /**
         * Always follow axis order.
         * @type {Array.<string>}
         * @readOnly
         */
        this.dimensions = parallelModel.dimensions;

        /**
         * @type {module:zrender/core/BoundingRect}
         */
        this._rect;

        /**
         * @type {module:echarts/coord/parallel/ParallelModel}
         */
        this._model = parallelModel;

        this._init(parallelModel, ecModel, api);
    }

    Parallel.prototype = {

        type: 'parallel',

        constructor: Parallel,

        /**
         * Initialize cartesian coordinate systems
         * @private
         */
        _init: function (parallelModel, ecModel, api) {

            var dimensions = parallelModel.dimensions;
            var parallelAxisIndex = parallelModel.parallelAxisIndex;

            each(dimensions, function (dim, idx) {

                var axisIndex = parallelAxisIndex[idx];
                var axisModel = ecModel.getComponent('parallelAxis', axisIndex);

                var axis = this._axesMap.set(dim, new ParallelAxis(
                    dim,
                    axisHelper.createScaleByModel(axisModel),
                    [0, 0],
                    axisModel.get('type'),
                    axisIndex
                ));

                var isCategory = axis.type === 'category';
                axis.onBand = isCategory && axisModel.get('boundaryGap');
                axis.inverse = axisModel.get('inverse');

                // Injection
                axisModel.axis = axis;
                axis.model = axisModel;
                axis.coordinateSystem = axisModel.coordinateSystem = this;

            }, this);
        },

        /**
         * Update axis scale after data processed
         * @param  {module:echarts/model/Global} ecModel
         * @param  {module:echarts/ExtensionAPI} api
         */
        update: function (ecModel, api) {
            this._updateAxesFromSeries(this._model, ecModel);
        },

        /**
         * @override
         */
        containPoint: function (point) {
            var layoutInfo = this._makeLayoutInfo();
            var axisBase = layoutInfo.axisBase;
            var layoutBase = layoutInfo.layoutBase;
            var pixelDimIndex = layoutInfo.pixelDimIndex;
            var pAxis = point[1 - pixelDimIndex];
            var pLayout = point[pixelDimIndex];

            return pAxis >= axisBase
                && pAxis <= axisBase + layoutInfo.axisLength
                && pLayout >= layoutBase
                && pLayout <= layoutBase + layoutInfo.layoutLength;
        },

        /**
         * Update properties from series
         * @private
         */
        _updateAxesFromSeries: function (parallelModel, ecModel) {
            ecModel.eachSeries(function (seriesModel) {

                if (!parallelModel.contains(seriesModel, ecModel)) {
                    return;
                }

                var data = seriesModel.getData();

                each(this.dimensions, function (dim) {
                    var axis = this._axesMap.get(dim);
                    axis.scale.unionExtentFromData(data, dim);
                    axisHelper.niceScaleExtent(axis.scale, axis.model);
                }, this);
            }, this);
        },

        /**
         * Resize the parallel coordinate system.
         * @param {module:echarts/coord/parallel/ParallelModel} parallelModel
         * @param {module:echarts/ExtensionAPI} api
         */
        resize: function (parallelModel, api) {
            this._rect = layoutUtil.getLayoutRect(
                parallelModel.getBoxLayoutParams(),
                {
                    width: api.getWidth(),
                    height: api.getHeight()
                }
            );

            this._layoutAxes();
        },

        /**
         * @return {module:zrender/core/BoundingRect}
         */
        getRect: function () {
            return this._rect;
        },

        /**
         * @private
         */
        _makeLayoutInfo: function () {
            var parallelModel = this._model;
            var rect = this._rect;
            var xy = ['x', 'y'];
            var wh = ['width', 'height'];
            var layout = parallelModel.get('layout');
            var pixelDimIndex = layout === 'horizontal' ? 0 : 1;
            var layoutLength = rect[wh[pixelDimIndex]];
            var layoutExtent = [0, layoutLength];
            var axisCount = this.dimensions.length;

            var axisExpandWidth = restrict(parallelModel.get('axisExpandWidth'), layoutExtent);
            var axisExpandCount = restrict(parallelModel.get('axisExpandCount') || 0, [0, axisCount]);
            var axisExpandable = parallelModel.get('axisExpandable')
                && axisCount > 3
                && axisCount > axisExpandCount
                && axisExpandCount > 1
                && axisExpandWidth > 0
                && layoutLength > 0;

            // `axisExpandWindow` is According to the coordinates of [0, axisExpandLength],
            // for sake of consider the case that axisCollapseWidth is 0 (when screen is narrow),
            // where collapsed axes should be overlapped.
            var axisExpandWindow = parallelModel.get('axisExpandWindow');
            var winSize;
            if (!axisExpandWindow) {
                winSize = restrict(axisExpandWidth * (axisExpandCount - 1), layoutExtent);
                var axisExpandCenter = parallelModel.get('axisExpandCenter') || mathFloor(axisCount / 2);
                axisExpandWindow = [axisExpandWidth * axisExpandCenter - winSize / 2];
                axisExpandWindow[1] = axisExpandWindow[0] + winSize;
            }
            else {
                 winSize = restrict(axisExpandWindow[1] - axisExpandWindow[0], layoutExtent);
                 axisExpandWindow[1] = axisExpandWindow[0] + winSize;
            }

            var axisCollapseWidth = (layoutLength - winSize) / (axisCount - axisExpandCount);
            // Avoid axisCollapseWidth is too small.
            axisCollapseWidth < 3 && (axisCollapseWidth = 0);

            // Find the first and last indices > ewin[0] and < ewin[1].
            var winInnerIndices = [
                mathFloor(round(axisExpandWindow[0] / axisExpandWidth, 1)) + 1,
                mathCeil(round(axisExpandWindow[1] / axisExpandWidth, 1)) - 1
            ];

            // Pos in ec coordinates.
            var axisExpandWindow0Pos = axisCollapseWidth / axisExpandWidth * axisExpandWindow[0];

            return {
                layout: layout,
                pixelDimIndex: pixelDimIndex,
                layoutBase: rect[xy[pixelDimIndex]],
                layoutLength: layoutLength,
                axisBase: rect[xy[1 - pixelDimIndex]],
                axisLength: rect[wh[1 - pixelDimIndex]],
                axisExpandable: axisExpandable,
                axisExpandWidth: axisExpandWidth,
                axisCollapseWidth: axisCollapseWidth,
                axisExpandWindow: axisExpandWindow,
                axisCount: axisCount,
                winInnerIndices: winInnerIndices,
                axisExpandWindow0Pos: axisExpandWindow0Pos
            };
        },

        /**
         * @private
         */
        _layoutAxes: function () {
            var rect = this._rect;
            var axes = this._axesMap;
            var dimensions = this.dimensions;
            var layoutInfo = this._makeLayoutInfo();
            var layout = layoutInfo.layout;

            axes.each(function (axis) {
                var axisExtent = [0, layoutInfo.axisLength];
                var idx = axis.inverse ? 1 : 0;
                axis.setExtent(axisExtent[idx], axisExtent[1 - idx]);
            });

            each(dimensions, function (dim, idx) {
                var posInfo = (layoutInfo.axisExpandable
                    ? layoutAxisWithExpand : layoutAxisWithoutExpand
                )(idx, layoutInfo);

                var positionTable = {
                    horizontal: {
                        x: posInfo.position,
                        y: layoutInfo.axisLength
                    },
                    vertical: {
                        x: 0,
                        y: posInfo.position
                    }
                };
                var rotationTable = {
                    horizontal: PI / 2,
                    vertical: 0
                };

                var position = [
                    positionTable[layout].x + rect.x,
                    positionTable[layout].y + rect.y
                ];

                var rotation = rotationTable[layout];
                var transform = matrix.create();
                matrix.rotate(transform, transform, rotation);
                matrix.translate(transform, transform, position);

                // TODO
                // tick等排布信息。

                // TODO
                // 根据axis order 更新 dimensions顺序。

                this._axesLayout[dim] = {
                    position: position,
                    rotation: rotation,
                    transform: transform,
                    axisNameAvailableWidth: posInfo.axisNameAvailableWidth,
                    axisLabelShow: posInfo.axisLabelShow,
                    nameTruncateMaxWidth: posInfo.nameTruncateMaxWidth,
                    tickDirection: 1,
                    labelDirection: 1,
                    labelInterval: axes.get(dim).getLabelInterval()
                };
            }, this);
        },

        /**
         * Get axis by dim.
         * @param {string} dim
         * @return {module:echarts/coord/parallel/ParallelAxis} [description]
         */
        getAxis: function (dim) {
            return this._axesMap.get(dim);
        },

        /**
         * Convert a dim value of a single item of series data to Point.
         * @param {*} value
         * @param {string} dim
         * @return {Array}
         */
        dataToPoint: function (value, dim) {
            return this.axisCoordToPoint(
                this._axesMap.get(dim).dataToCoord(value),
                dim
            );
        },

        /**
         * Travel data for one time, get activeState of each data item.
         * @param {module:echarts/data/List} data
         * @param {Functio} cb param: {string} activeState 'active' or 'inactive' or 'normal'
         *                            {number} dataIndex
         * @param {Object} context
         */
        eachActiveState: function (data, callback, context) {
            var dimensions = this.dimensions;
            var axesMap = this._axesMap;
            var hasActiveSet = this.hasAxisBrushed();

            for (var i = 0, len = data.count(); i < len; i++) {
                var values = data.getValues(dimensions, i);
                var activeState;

                if (!hasActiveSet) {
                    activeState = 'normal';
                }
                else {
                    activeState = 'active';
                    for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
                        var dimName = dimensions[j];
                        var state = axesMap.get(dimName).model.getActiveState(values[j], j);

                        if (state === 'inactive') {
                            activeState = 'inactive';
                            break;
                        }
                    }
                }

                callback.call(context, activeState, i);
            }
        },

        /**
         * Whether has any activeSet.
         * @return {boolean}
         */
        hasAxisBrushed: function () {
            var dimensions = this.dimensions;
            var axesMap = this._axesMap;
            var hasActiveSet = false;

            for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
                if (axesMap.get(dimensions[j]).model.getActiveState() !== 'normal') {
                    hasActiveSet = true;
                }
            }

            return hasActiveSet;
        },

        /**
         * Convert coords of each axis to Point.
         *  Return point. For example: [10, 20]
         * @param {Array.<number>} coords
         * @param {string} dim
         * @return {Array.<number>}
         */
        axisCoordToPoint: function (coord, dim) {
            var axisLayout = this._axesLayout[dim];
            return graphic.applyTransform([coord, 0], axisLayout.transform);
        },

        /**
         * Get axis layout.
         */
        getAxisLayout: function (dim) {
            return zrUtil.clone(this._axesLayout[dim]);
        },

        /**
         * @param {Array.<number>} point
         * @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}.
         */
        getSlidedAxisExpandWindow: function (point) {
            var layoutInfo = this._makeLayoutInfo();
            var pixelDimIndex = layoutInfo.pixelDimIndex;
            var axisExpandWindow = layoutInfo.axisExpandWindow.slice();
            var winSize = axisExpandWindow[1] - axisExpandWindow[0];
            var extent = [0, layoutInfo.axisExpandWidth * (layoutInfo.axisCount - 1)];

            // Out of the area of coordinate system.
            if (!this.containPoint(point)) {
                return {behavior: 'none', axisExpandWindow: axisExpandWindow};
            }

            // Conver the point from global to expand coordinates.
            var pointCoord = point[pixelDimIndex] - layoutInfo.layoutBase - layoutInfo.axisExpandWindow0Pos;

            // For dragging operation convenience, the window should not be
            // slided when mouse is the center area of the window.
            var delta;
            var behavior = 'slide';
            var axisCollapseWidth = layoutInfo.axisCollapseWidth;
            var triggerArea = this._model.get('axisExpandSlideTriggerArea');
            // But consider touch device, jump is necessary.
            var useJump = triggerArea[0] != null;

            if (axisCollapseWidth) {
                if (useJump && axisCollapseWidth && pointCoord < winSize * triggerArea[0]) {
                    behavior = 'jump';
                    delta = pointCoord - winSize * triggerArea[2];
                }
                else if (useJump && axisCollapseWidth && pointCoord > winSize * (1 - triggerArea[0])) {
                    behavior = 'jump';
                    delta = pointCoord - winSize * (1 - triggerArea[2]);
                }
                else {
                    (delta = pointCoord - winSize * triggerArea[1]) >= 0
                        && (delta = pointCoord - winSize * (1 - triggerArea[1])) <= 0
                        && (delta = 0);
                }
                delta *= layoutInfo.axisExpandWidth / axisCollapseWidth;
                delta
                    ? sliderMove(delta, axisExpandWindow, extent, 'all')
                    // Avoid nonsense triger on mousemove.
                    : (behavior = 'none');
            }
            // When screen is too narrow, make it visible and slidable, although it is hard to interact.
            else {
                var winSize = axisExpandWindow[1] - axisExpandWindow[0];
                var pos = extent[1] * pointCoord / winSize;
                axisExpandWindow = [mathMax(0, pos - winSize / 2)];
                axisExpandWindow[1] = mathMin(extent[1], axisExpandWindow[0] + winSize);
                axisExpandWindow[0] = axisExpandWindow[1] - winSize;
            }

            return {
                axisExpandWindow: axisExpandWindow,
                behavior: behavior
            };
        }
    };

    function restrict(len, extent) {
        return mathMin(mathMax(len, extent[0]), extent[1]);
    }

    function layoutAxisWithoutExpand(axisIndex, layoutInfo) {
        var step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1);
        return {
            position: step * axisIndex,
            axisNameAvailableWidth: step,
            axisLabelShow: true
        };
    }

    function layoutAxisWithExpand(axisIndex, layoutInfo) {
        var layoutLength = layoutInfo.layoutLength;
        var axisExpandWidth = layoutInfo.axisExpandWidth;
        var axisCount = layoutInfo.axisCount;
        var axisCollapseWidth = layoutInfo.axisCollapseWidth;
        var winInnerIndices = layoutInfo.winInnerIndices;

        var position;
        var axisNameAvailableWidth = axisCollapseWidth;
        var axisLabelShow = false;
        var nameTruncateMaxWidth;

        if (axisIndex < winInnerIndices[0]) {
            position = axisIndex * axisCollapseWidth;
            nameTruncateMaxWidth = axisCollapseWidth;
        }
        else if (axisIndex <= winInnerIndices[1]) {
            position = layoutInfo.axisExpandWindow0Pos
                + axisIndex * axisExpandWidth - layoutInfo.axisExpandWindow[0];
            axisNameAvailableWidth = axisExpandWidth;
            axisLabelShow = true;
        }
        else {
            position = layoutLength - (axisCount - 1 - axisIndex) * axisCollapseWidth;
            nameTruncateMaxWidth = axisCollapseWidth;
        }

        return {
            position: position,
            axisNameAvailableWidth: axisNameAvailableWidth,
            axisLabelShow: axisLabelShow,
            nameTruncateMaxWidth: nameTruncateMaxWidth
        };
    }

    return Parallel;
});